/**
 * Copyright (C) 2007 The AsyncMail Group <asyncmail@googlegroups.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.asyncmail.smtpserver.handler;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.springframework.beans.factory.annotation.Required;

import com.googlecode.asyncmail.common.DSNStatus;
import com.googlecode.asyncmail.common.ExtensibleHandler;
import com.googlecode.asyncmail.common.MailEnvelope;
import com.googlecode.asyncmail.common.WiringException;
import com.googlecode.asyncmail.smtp.SMTPResponseImpl;
import com.googlecode.asyncmail.smtp.SMTPRetCode;
import com.googlecode.asyncmail.smtpserver.SMTPSession;
import com.googlecode.asyncmail.smtpserver.hook.HookResult;
import com.googlecode.asyncmail.smtpserver.hook.HookResultHook;
import com.googlecode.asyncmail.smtpserver.hook.HookReturnCode;
import com.googlecode.asyncmail.smtpserver.hook.MessageHook;

import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;

public final class DataLineMessageHookHandler implements DataLineFilter,
        ExtensibleHandler {

    private List<MessageHook> messageHandlers;

    private List<HookResultHook> rHooks;

    private Log logger;

    @Required
    public void setLogger(Log logger) {
        this.logger = logger;
    }

    protected Log getLogger() {
        return logger;
    }

    private static final String DATA_MIMEMESSAGE_OUTPUTSTREAM = "com.googlecode.asyncmail.smtpserver.handler.DataLineMessageHandler.DATA_MIMEMESSAGE_OUTPUTSTREAM";

    private final static String SOFTWARE_TYPE = "AsyncMail SMTP Server 0.1";

    /**
     * @see com.googlecode.asyncmail.smtpserver.core.DataLineFilter#onLine(com.googlecode.asyncmail.smtpserver.SMTPSession,
     *      byte[], com.googlecode.asyncmail.smtpserver.LineHandler)
     */
    @SuppressWarnings("unchecked")
    public void onLine(SMTPSession session, byte[] line, LineHandler next) {
        OutputStream out = (OutputStream) session.getState().get(
                DATA_MIMEMESSAGE_OUTPUTSTREAM);
        if (out == null) {

            try {
                // TODO not correct: we have to save it somewhere.
                // Maybe we should introduce a pluggable factory for the
                // incoming
                // stream.
                MailEnvelope envelope = (MailEnvelope) session.getState().get(
                        SMTPSession.MAIL_ENVELOPE);
                out = envelope.getBodyOutputStream();
                // = mmiss.getWritableOutputStream();

                // Prepend output headers with out Received
                InternetHeaders mh = createNewReceivedMailHeaders(session);
                for (Enumeration en = mh.getAllHeaderLines(); en
                        .hasMoreElements();) {
                    out.write(en.nextElement().toString().getBytes());
                    out.write("\r\n".getBytes());
                }

                session.getState().put(DATA_MIMEMESSAGE_OUTPUTSTREAM, out);

            } catch (IOException e) {
                getLogger()
                        .warn(
                                "Error creating temporary outputstream for incoming data",
                                e);
                session.writeSMTPResponse(new SMTPResponseImpl(
                        SMTPRetCode.LOCAL_ERROR,
                        "Unexpected error preparing to receive DATA."));
            } catch (MessagingException e) {
                getLogger()
                        .warn(
                                "Error creating mimemessagesource for incoming data",
                                e);
                session.writeSMTPResponse(new SMTPResponseImpl(
                        SMTPRetCode.LOCAL_ERROR,
                        "Unexpected error preparing to receive DATA."));
            }

        }

        try {
            // 46 is "."
            // Stream terminated
            if (line.length == 3 && line[0] == 46) {
                out.flush();
                out.close();

                MailEnvelope mailEnvelope = (MailEnvelope) session.getState()
                        .get(SMTPSession.MAIL_ENVELOPE);
                // MailImpl mail =
                // new
                // MailImpl(session.getConfigurationData().getMailServer().getId(),
                // (MailAddress) session.getState().get(SMTPSession.SENDER),
                // recipientCollection);
                // MimeMessageCopyOnWriteProxy mimeMessageCopyOnWriteProxy =
                // null;
                try {
                    // mimeMessageCopyOnWriteProxy = new
                    // MimeMessageCopyOnWriteProxy(mmiss);
                    // mail.setMessage(mimeMessageCopyOnWriteProxy);
                    //                    
                    processExtensions(session, mailEnvelope);
                    next.onLine(session, line);

                } catch (Exception e) {
                    // TODO probably return a temporary problem
                    getLogger()
                            .info("Unexpected error handling DATA stream", e);
                    session.writeSMTPResponse(new SMTPResponseImpl(
                            SMTPRetCode.LOCAL_ERROR,
                            "Unexpected error handling DATA stream."));
                } finally {
                    // ContainerUtil.dispose(mimeMessageCopyOnWriteProxy);
                    // ContainerUtil.dispose(mmiss);
                    // ContainerUtil.dispose(mail);
                }

                // DotStuffing.
            } else if (line[0] == 46 && line[1] == 46) {
                out.write(line, 1, line.length - 1);
                // Standard write
            } else {
                // TODO: maybe we should handle the Header/Body recognition here
                // and if needed let a filter to cache the headers to apply some
                // transormation before writing them to output.
                out.write(line);
            }
            out.flush();
        } catch (IOException e) {
            SMTPResponseImpl response;
            response = new SMTPResponseImpl(SMTPRetCode.LOCAL_ERROR, DSNStatus
                    .getStatus(DSNStatus.TRANSIENT, DSNStatus.UNDEFINED_STATUS)
                    + " Error processing message: " + e.getMessage());

            getLogger().error("Unknown error occurred while processing DATA.",
                    e);
            session.writeSMTPResponse(response);
            return;
        }
    }

    /**
     * @param session
     */
    private void processExtensions(SMTPSession session, MailEnvelope mail) {
        if (mail != null && messageHandlers != null) {
            try {
                int count = messageHandlers.size();
                for (int i = 0; i < count; i++) {
                    MessageHook rawHandler = messageHandlers.get(i);
                    getLogger()
                            .debug("executing message handler " + rawHandler);
                    HookResult hRes = rawHandler.onMessage(session, mail);

                    if (rHooks != null) {
                        for (int i2 = 0; i2 < rHooks.size(); i2++) {
                            HookResultHook rHook = rHooks.get(i2);
                            getLogger().debug("executing hook " + rHook);
                            hRes = rHook
                                    .onHookResult(session, hRes, rawHandler);
                        }
                    }

                    if (hRes == null
                            || hRes.getResult() == HookReturnCode.DECLINED) {
                        hRes = new HookResult(HookReturnCode.OK, DSNStatus
                                .getStatus(DSNStatus.SUCCESS,
                                        DSNStatus.CONTENT_OTHER)
                                + " Message received");
                    }

                    SMTPResponseImpl response = AbstractHookableCmdHandler
                            .calcDefaultSMTPResponse(hRes);

                    // if the response is received, stop processing of command
                    // handlers
                    if (response != null) {
                        session.writeSMTPResponse(response);
                        break;
                    }
                }
            } finally {
                // Dispose the mail object and remove it
                if (mail != null) {
                    mail = null;
                }
                // do the clean up
                session.resetState();
            }
        }
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.ExtensibleHandler#wireExtensions(java.lang.Class,
     *      java.util.List)
     */
    @SuppressWarnings("unchecked")
    public void wireExtensions(Class<?> interfaceName, List<?> extension)
            throws WiringException {
        if (MessageHook.class.equals(interfaceName)) {
            this.messageHandlers = (List<MessageHook>) extension;
            if (messageHandlers.size() == 0) {
                if (getLogger().isErrorEnabled()) {
                    getLogger()
                            .error(
                                    "No messageHandler configured. Check that SendMailHandler is configured in the SMTPHandlerChain");
                }
                throw new WiringException("No messageHandler configured");
            }
        } else if (HookResultHook.class.equals(interfaceName)) {
            this.rHooks = (List<HookResultHook>) extension;
        }
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.ExtensibleHandler#getMarkerInterfaces()
     */
    public List<Class<?>> getMarkerInterfaces() {
        List<Class<?>> classes = new LinkedList<Class<?>>();
        classes.add(MessageHook.class);
        classes.add(HookResultHook.class);
        return classes;
    }

    /**
     * @param session
     * @param headerLineBuffer
     * @return
     * @throws MessagingException
     */
    private InternetHeaders createNewReceivedMailHeaders(SMTPSession session)
            throws MessagingException {
        StringBuffer headerLineBuffer = new StringBuffer(512);
        InternetHeaders newHeaders = new InternetHeaders();

        String heloMode = (String) session.getConnectionState().get(
                SMTPSession.CURRENT_HELO_MODE);
        String heloName = (String) session.getConnectionState().get(
                SMTPSession.CURRENT_HELO_NAME);

        // Put our Received header first
        headerLineBuffer.append("Received: from ").append(
                session.getRemoteHost());

        if (heloName != null) {
            headerLineBuffer.append(" (").append(heloMode).append(" ").append(
                    heloName).append(") ");
        }

        headerLineBuffer.append(" ([").append(session.getRemoteIPAddress())
                .append("])");

        newHeaders.addHeaderLine(headerLineBuffer.toString());
        headerLineBuffer.delete(0, headerLineBuffer.length());

        headerLineBuffer.append("          by ").append(
                session.getConfigurationData().getHelloName()).append(" (")
                .append(SOFTWARE_TYPE).append(") with ");

        // Check if EHLO was used
        if ("EHLO".equals(heloMode)) {
            // Not succesfull auth
            if (session.getUser() == null) {
                headerLineBuffer.append("ESMTP");
            } else {
                // See RFC3848
                // The new keyword "ESMTPA" indicates the use of ESMTP when the
                // SMTP
                // AUTH [3] extension is also used and authentication is
                // successfully
                // achieved.
                headerLineBuffer.append("ESMTPA");
            }
        } else {
            headerLineBuffer.append("SMTP");
        }

        headerLineBuffer.append(" ID ").append(session.getSessionID());

        MailEnvelope mailEnvelope = (MailEnvelope) session.getState().get(
                SMTPSession.MAIL_ENVELOPE);
        if (mailEnvelope.getRecipients().size() == 1) {
            // Only indicate a recipient if they're the only recipient
            // (prevents email address harvesting and large headers in
            // bulk email)
            newHeaders.addHeaderLine(headerLineBuffer.toString());
            headerLineBuffer.delete(0, headerLineBuffer.length());
            headerLineBuffer.append("          for <").append(
                    mailEnvelope.getRecipients().get(0).toString())
                    .append(">;");
            newHeaders.addHeaderLine(headerLineBuffer.toString());
            headerLineBuffer.delete(0, headerLineBuffer.length());
        } else {
            // Put the ; on the end of the 'by' line
            headerLineBuffer.append(";");
            newHeaders.addHeaderLine(headerLineBuffer.toString());
            headerLineBuffer.delete(0, headerLineBuffer.length());
        }
        headerLineBuffer = null;
        // TODO enable rfc822 formatting.
        // newHeaders.addHeaderLine(" " + rfc822DateFormat.format(new Date()));
        return newHeaders;
    }

}